//
// raytrace.cpp
//

#define _CRT_SECURE_NO_WARNINGS
#include "matm.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <math.h>

#include "sphere.h"
#include "Color.h"
#include "Light.h"
#include "vector3.h"
#include "Point.h"
#include <utility>


using namespace std;

int g_width;
int g_height;

struct Ray
{

	/// <summary>
	/// point
	/// </summary>
	Point origin;
	/// <summary>
	/// vector
	/// </summary>
	vector3 direction;
};

// TODO: add structs for spheres, lights and anything else you may need.

vector<vec4> g_colors;

float g_left;
float g_right;
float g_top;
float g_bottom;
/************************************************************************/
/* positive                                                             */
/************************************************************************/
float g_near;

vector<Sphere*> spheres;
vector<Light> lights;

Color* backgroundColor=NULL;
vec3 ambientIntensity;

string outputFileName;


// -------------------------------------------------------------------
// Input file parsing

vec4 toVec4(const string& s1, const string& s2, const string& s3)
{
	stringstream ss(s1 + " " + s2 + " " + s3);
	vec4 result;
	ss >> result.x >> result.y >> result.z;
	result.w = 1.0f;
	return result;
}

float toFloat(const string& s)
{
	stringstream ss(s);
	float f;
	ss >> f;
	return f;
}

void parseLine(const vector<string>& vs)
{
	if(vs[0]=="NEAR")
	{
		g_near=toFloat(vs[1]);
	}
	else if(vs[0]=="LEFT")
	{
		g_left=toFloat(vs[1]);
	}
	else if(vs[0]=="RIGHT")
	{
		g_right=toFloat(vs[1]);
	}
	else if(vs[0]=="BOTTOM")
	{
		g_bottom=toFloat(vs[1]);
	}
	else if(vs[0]=="TOP")
	{
		g_top=toFloat(vs[1]);
	}
	else if (vs[0] == "RES")
	{
		g_width = (int)toFloat(vs[1]);
		g_height = (int)toFloat(vs[2]);
		g_colors.resize(g_width * g_height);
	}
	else if(vs[0]=="SPHERE")
	{
		Sphere* s=new Sphere();

		int i=0;
		for(;i<3;i++)
			s->Position[i]=toFloat(vs[2+i]);

		i=0;
		for(;i<3;i++)
			s->Scale[i]=toFloat(vs[5+i]);

		s->Color.r=toFloat(vs[8]);
		s->Color.g=toFloat(vs[9]);
		s->Color.b=toFloat(vs[10]);


		s->KAmbient=toFloat(vs[11]);		
		s->KDiffuse=toFloat(vs[12]);		
		s->KSpecular=toFloat(vs[13]);		
		s->KR=toFloat(vs[14]);

		s->computeTransformMatrix();

		s->ShininessFactor=toFloat(vs[15]);
		spheres.push_back(s);
	}
	else if(vs[0]=="BACK")
	{
		backgroundColor=new Color;
		backgroundColor->r=toFloat(vs[1]);
		backgroundColor->g=toFloat(vs[2]);
		backgroundColor->b=toFloat(vs[3]);

	}
	else if(vs[0]=="AMBIENT")
	{
		ambientIntensity[0]=toFloat(vs[1]);
		ambientIntensity[1]=toFloat(vs[2]);
		ambientIntensity[2]=toFloat(vs[3]);

	}
	else if(vs[0]=="LIGHT")
	{
		Light light;
		light.Position.x=toFloat(vs[2]);
		light.Position.y=toFloat(vs[3]);
		light.Position.z=toFloat(vs[4]);

		light.Intensity[0]=toFloat(vs[5]);
		light.Intensity[1]=toFloat(vs[6]);
		light.Intensity[2]=toFloat(vs[7]);

		lights.push_back(light);
	}
	else if(vs[0]=="OUTPUT")
		outputFileName=vs[1];
}

void loadFile(const char* filename)
{
	ifstream is(filename);
	if (is.fail())
	{
		cout << "Could not open file " << filename << endl;
		exit(1);
	}
	string s;
	vector<string> vs;
	while(!is.eof())
	{
		vs.clear();
		getline(is, s);
		istringstream iss(s);
		while (!iss.eof())
		{
			string sub;
			iss >> sub;
			vs.push_back(sub);
		}
		parseLine(vs);
	}
}


// -------------------------------------------------------------------
// Utilities

void setColor(int ix, int iy,Color color)
{
	int iy2 = g_height - iy - 1; // Invert iy coordinate.
	g_colors[iy2 * g_width + ix].x = color.r;
	g_colors[iy2 * g_width + ix].y = color.g;
	g_colors[iy2 * g_width + ix].z = color.b;
}


// -------------------------------------------------------------------
// Intersection routine

// TODO: add your ray-sphere intersection routine here.



// -------------------------------------------------------------------
// Ray tracing

//Color getAmbientLight(Sphere sphere)
//{
//	return ambientColor*sphere.KAmbient;
//}

///<summary>
///find the intersection point between the line and sphere.
///</summary>
vector<pair<Point,double>> findIntersectionToDefaultSphere(Point point1, Point point2)
{
	vector3 e=point1.toVector();
	//Point e=point1;
	vector3 d=point2-point1;

	double part1=pow(dot(d,e),2);

	double part2_2=dot(e,e)-1;
	double part2=dot(d,d) *(part2_2);

	double delta=part1 -part2;

	if(delta<0)
		return vector<pair<Point,double>>();
	else if(delta==0)
	{
		float t=(-((vec4)d).dotProduct(e))/((vec4)d).dotProduct(d);


		Point p((vec4)point1+(vec4)d*t);
		vector<pair<Point,double>> list;
		list.push_back(make_pair(p,t));
		return list;
	}
	else
	{
		vector<pair<Point,double>> list;
		float t=(-((vec4)d).dotProduct(e)-sqrt(delta))/((vec4)d).dotProduct(d);

		Point p((vec4)point1+(vec4)d*t);
		list.push_back(make_pair(p,t));

		t=(-((vec4)d).dotProduct(e)+sqrt(delta))/((vec4)d).dotProduct(d);

		p=Point((vec4)point1+(vec4)d*t);
		list.push_back(make_pair(p,t));

		return list;
	}
}

//vector<Point> findIntersectionToDefaultSphere(Point point1, Point point2)
//{
//	vec3 e(point1.x,point1.y,point1.z);
//	vec3 d(point2.x,point2.y,point2.z);
//	d=d-e;
//
//	float delta= pow(dot(d,e),2)-dot(d,d)*(dot(d,e)-1);
//
//	if(delta<0)
//		return vector<Point>();
//	else if(delta==0)
//	{
//		float t=(-dot(d,e))/dot(d,e);
//
//
//		Point p((vec4)point1+(vec4)d*t);
//		vector<Point> list;
//		list.push_back(p);
//		return list;
//	}
//	else
//	{
//		vector<Point> list;
//		float t=(-dot(d,e)-sqrt(delta))/dot(d,d);
//
//		Point p;
//		p.x=point1.x+d.x*t;
//		p.y=point1.y+d.y*t;
//		p.z=point1.z+d.z*t;
//		list.push_back(p);
//
//		t=(-dot(d,e)+sqrt(delta))/dot(d,d);
//
//		p.x=point1.x+d.x*t;
//		p.y=point1.y+d.y*t;
//		p.z=point1.z+d.z*t;
//		list.push_back(p);
//
//		return list;
//	}
//}

///<summary>
///find the intersection point between the line and sphere.
///</summary>
vector<pair<Point,double>> findIntersection(Point point1, Point point2,  Sphere* sphere)
{
	//vec4 tp1= sphere->transformMatrixInverse*point1; //point
	//vector3 e(tp1.x,tp1.y,tp1.z);
	//vector3 d(sphere->transformMatrixInverse*point2-tp1);

	vector<pair<Point,double>> list=findIntersectionToDefaultSphere(sphere->transformMatrixInverse*point1, sphere->transformMatrixInverse*point2);

	for (int i=0;i<list.size();i++)
		list[i].first=sphere->transformMatrix*list[i].first;

	return list;
}

///<summary>return normalized normal vector.</summary>
vector3 getNormal(Sphere* sphere, Point point)
{
	vec4 p= sphere->transformMatrixInverse*point;
	vector3 normal(p.x,p.y,p.z);
	normal=sphere->transformMatrixInverse*normal;
	normal= normalize(normal);
	return normal;
}

///<summary>ambient + sum(diffuse+specular)</summary>
Color localIlluminate(Point intersection, Sphere* sphere)
{
	vector3 normal=getNormal(sphere, intersection);


	Color color;
	for (int i=0;i<lights.size();i++)
	{
		//Need to check whether the point is lit by the light.
		//Due to inaccuracy in transformation and e will be unit vector,
		//if we use ee-1 to determine number of solutions,
		//has-solution case may become no solution.
		//Therefore, just use angle between normal and point-to-light vector
		//to determine whether the light is blocked.

		/*
		This method intails less computation and thus more accurate.
		*/
		vector3 l=lights[i].Position-intersection; //point to light
		l=normalize(l);
		float dp=dot(normal,l);

		bool isLightBlocked=dp<0;

		for (int j=0;j<spheres.size();j++)
		{
			if(isLightBlocked)
				break;

			if(spheres[j]==sphere)
				continue;

			vector<pair<Point,double>> s= findIntersection(intersection,lights[i].Position,spheres[j]);
			if(s.size()>0)
			{
				if(s[0].second<0)
					s.erase(s.begin());
				if(s[0].second<0)
					s.erase(s.begin());

				if(s.size()>0)
					isLightBlocked=true;
			}
		}


		if(isLightBlocked==false)
		{
			vec3 intensity=sphere->KDiffuse*lights[i].Intensity*dp;

			assert(intensity[0]>=0);
			assert(intensity[1]>=0);
			assert(intensity[2]>=0);

			Color diffuse;
			diffuse.r=sphere->Color.r*intensity[0];
			diffuse.g=sphere->Color.g*intensity[1];
			diffuse.b=sphere->Color.b*intensity[2];

			vector3 r= 2*((vec4)normal*(dot(normal,l)))-l;
			r=normalize(r);

			vector3 v(-intersection.x,-intersection.y,-intersection.z);
			v=normalize(v);
			dp=dot(r,v);
			if(dp<0)
				dp=0;
			intensity=lights[i].Intensity*sphere->KSpecular*pow(dp,sphere->ShininessFactor);
			assert(intensity[0]>=0);
			assert(intensity[1]>=0);
			assert(intensity[2]>=0);

			Color specular;
			specular.r=intensity[0];
			specular.g=intensity[1];
			specular.b=intensity[2];

			color=color+ diffuse+specular;
		}
	}


	Color ambientColor;
	ambientColor.r=sphere->KAmbient*sphere->Color.r*ambientIntensity[0];
	ambientColor.g=sphere->KAmbient*sphere->Color.g*ambientIntensity[1];
	ambientColor.b=sphere->KAmbient*sphere->Color.b*ambientIntensity[2];

	return color+ambientColor;
}

/***********************************************************************
return backgroundColor* or the combined color of the ray.
reflectionCountdown: at most shoot how many reflect rays.
***********************************************************************/
Color* trace(const Ray& ray,double minT, int reflectionCountdown)
{
	//the ray may intersect multiple spheres, choose the closest one.

	pair<Point,double> p;
	Sphere* sphere=NULL;
	//bool hasSolution=false;

	for (int i=0;i<spheres.size();i++)
	{
		vector<pair<Point,double>> solution=findIntersection(ray.origin,(vec4)ray.origin+ray.direction,spheres[i]);

		if(solution.size()==0)
			continue;

		if(solution[0].second<minT)
			solution.erase(solution.begin());
		if(solution[0].second<minT)
			solution.erase(solution.begin());

		//had intersections but they are all in front of image plane, removed.
		if(solution.size()==0)
			continue;

		if(sphere==NULL || solution[0].second<p.second)
		{
			//hasSolution=true;
			p=solution[0];
			sphere=spheres[i];
		}
	}


	if(sphere==NULL) //the ray doesn't intersect any sphere.
		return backgroundColor;
	else
	{
		Point intersection=p.first;
		Color* c=new Color(localIlluminate(intersection,sphere));

		vector3 normal=getNormal(sphere,intersection);
		vec4 l=normalize(ray.direction);
		vector3 newRayDirection= -2*(dot(normal,l)*((vec4)normal))+l;

		if(reflectionCountdown>0)
		{
			Ray reflectedRay;
			reflectedRay.origin=intersection;
			reflectedRay.direction=newRayDirection;

			Color* reflectColor= trace(reflectedRay,0.0001,reflectionCountdown-1);
			if(reflectColor!=backgroundColor)
			{
				reflectColor->r*=sphere->KR;
				reflectColor->g*=sphere->KR;
				reflectColor->b*=sphere->KR;

				*c=(*c)+(*reflectColor);

				delete reflectColor;
			}
		}
		return c;
	}
}

vec4 getRayDirection(int x, int y)
{
	float u=g_left+(g_right-g_left)*x/(g_width-1);
	float v=g_bottom+(g_top-g_bottom)*y/(g_height-1);

	vec4 direction;
	direction = vec4(u, v, -g_near, 0.0f);
	//printf("%f, %f\n",u,v);
	return direction;
}

void renderPixel(int x, int y)
{
	//if(x==333 && y==360) //y from down to up, x from left to right
	//	//return;
	//{
		Ray ray;
		ray.origin = Point(0.0f, 0.0f, 0.0f);
		ray.direction = getRayDirection(x, y);
		Color* color = trace(ray,1,3);

		setColor(x, y, *color);



		if(color!=backgroundColor)
			delete color;
	//}
}

void render()
{
	for (int iy = 0; iy < g_height; iy++)
		for (int ix = 0; ix < g_width; ix++)
			renderPixel(ix, iy);
}


// -------------------------------------------------------------------
// PPM saving

void savePPM(int Width, int Height, char* fname, unsigned char* pixels) 
{
	FILE *fp;
	const int maxVal=255;

	printf("Saving image %s: %d x %d\n", fname, Width, Height);
	fp = fopen(fname,"wb");
	if (!fp) {
		printf("Unable to open file '%s'\n", fname);
		return;
	}
	fprintf(fp, "P6\n");
	fprintf(fp, "%d %d\n", Width, Height);
	fprintf(fp, "%d\n", maxVal);

	for(int j = 0; j < Height; j++) {
		fwrite(&pixels[j*Width*3], 3, Width, fp);
	}

	fclose(fp);
}

void saveFile()
{
	// Convert color components from floats to unsigned chars.
	// TODO: clamp values if out of range.
	unsigned char* buf = new unsigned char[g_width * g_height * 3];
	for (int y = 0; y < g_height; y++)
		for (int x = 0; x < g_width; x++)
			for (int i = 0; i < 3; i++)
				buf[y*g_width*3+x*3+i] = (unsigned char)(((double*)g_colors[y*g_width+x])[i] * 255.9f);

	// TODO: change file name based on input file name.

#if _DEBUG
	savePPM(g_width, g_height, "output.ppm", buf);
#else
	savePPM(g_width, g_height, const_cast<char*>(outputFileName.data()), buf);
#endif
	delete[] buf;
}


// -------------------------------------------------------------------
// Main

int main(int argc, char* argv[])
{
	//std::vector<Point> solution= findIntersectionToDefaultSphere(Point(-1,0,0),Point(0,1,0));




	if (argc < 2)
	{
		cout << "Usage: template-rt <input_file.txt>" << endl;
		exit(1);
	}
	loadFile(argv[1]);
	render();
	saveFile();


	for (int i=0;i<spheres.size();i++)
		delete spheres[i];

	delete backgroundColor;

	return 0;
}

